﻿using System;
using System.Drawing;
using System.Linq;
using System.Windows.Forms;

namespace WindowsFormsApplication1
{
    public class NodeViewController
    {
        readonly NodeLayoutModel model;
        readonly ILayoutView view;

        ViewMouseState state;

        bool added = false;
        public NodeViewController(NodeLayoutModel model, ILayoutView view)
        {
            this.model = model;
            this.view = view;
            state = new IdleState(model);
        }

        /// <summary>
        /// Вызывается при нажатии мышью.
        /// </summary>
        /// <param name="point">Координаты нажатия.</param>
        /// <param name="button">Флаги, определяющие нажатые кнопки мыши.</param>
        /// <param name="keyModifiers">Флаги, определяющие нажатые в данный момент кнопки на клавиатуре.</param>
        public void OnMouseDown(Point point, MouseButtons button, Keys keyModifiers)
        {
            var viewPoint = model.ClientToView(point);
            if (button == MouseButtons.Middle)
            {
                state = new SelectionBoxState(model, view, viewPoint);
                return;
            }

            var hitTest = TestMouseHit(viewPoint);
            switch (hitTest.hitType)
            {
                case HitType.ELEMENT:
                    state = new DraggingState(model, view, hitTest.element, viewPoint);
                    break;
                default:
                    model.SelectedElements.Clear();
                    state = new MovingState(model, view, point);
                    break;
            }
        }

        public void OnMouseMove(Point point)
        {
            state.ProcessMouseMovement(point);
        }

        public void OnMouseUp(Point point)
        {
            state.Leave();
            state = new IdleState(model);
            view.UpdateView();
        }

        public void OnMouseWheel(int delta)
        {
            if (delta == 0)
                return;
            if (delta > 0)
                model.ZoomIn();
            else
                model.ZoomOut();
            view.UpdateView();
        }

        /// <summary>
        /// Вычисляет тип попадания в координаты нажатия.
        /// </summary>
        /// <param name="viewPoint">К</param>
        HitTestResult TestMouseHit(Point viewPoint)
        {
            foreach (var element in model.GetVisibleElements())
            {
                var r = element.GetRectangle();
                if (r.Contains(viewPoint))
                    return HitTestResult.Element(element);
            }
            return HitTestResult.None();
        }

        public void OnSizeChanged(Size size)
        {
            model.ViewPortSize = size;
            view.UpdateView();
        }
    }

    public abstract class ViewMouseState
    {
        protected readonly NodeLayoutModel model;

        protected ViewMouseState(NodeLayoutModel model)
        {
            this.model = model;
        }

        public abstract void ProcessMouseMovement(Point p);

        public virtual void Leave()
        {
        }
    }

    /// <summary>
    /// Исходное состояние.
    /// </summary>
    public class IdleState : ViewMouseState
    {
        public IdleState(NodeLayoutModel model)
            : base(model)
        {
        }

        public override void ProcessMouseMovement(Point p)
        {
        }
    }

    /// <summary>
    /// Состояние перемещения области отображения.
    /// </summary>
    public class MovingState : ViewMouseState
    {
        readonly ILayoutView view;
        Point origin;

        public MovingState(NodeLayoutModel model, ILayoutView view, Point origin) : base(model)
        {
            this.view = view;
            this.origin = origin;
        }

        public override void ProcessMouseMovement(Point p)
        {
            model.ViewPortPosition.Offset((int) ((p.X - origin.X)/model.Scale), (int) ((p.Y - origin.Y)/model.Scale));
            origin = p;
            view.UpdateView();
        }
    }

    /// <summary>
    /// Состояние перемещения выделенного элемента.
    /// </summary>
    public class DraggingState : ViewMouseState
    {
        static readonly Size DRAG_TOLERANCE_SIZE = SystemMetrics.GetDragToleranceSize();

        readonly ILayoutView view;

        /// <summary>
        /// Координаты точки, в которой было зафиксировано первое нажатие мыши.
        /// </summary>
        readonly Point clickPoint;

        /// <summary>
        /// Флаг, равный true, если было начато перетаскивание, иначе false.
        /// </summary>
        bool isDragging;

        /// <summary>
        /// Координаты предыдущей точки, в которой находился указатель мыши.
        /// </summary>
        Point previousPoint;

        public DraggingState(NodeLayoutModel model, ILayoutView view, ElementModel element, Point origin) : base(model)
        {
            this.view = view;
            clickPoint = previousPoint = origin;
            if (!model.SelectedElements.Contains(element))
            {
                model.SelectedElements.Clear();
                model.SelectedElements.Add(element);
            }
            model.MoveToTop(element);
        }

        public override void ProcessMouseMovement(Point p)
        {
            var viewPoint = model.ClientToView(p);
            if (isDragging || (isDragging = ShouldStartDrag(viewPoint)))
                DoDrag(viewPoint);
            previousPoint = viewPoint;
        }

        public override void Leave()
        {
        }

        /// <summary>
        /// Выполняет перетаскивание.
        /// </summary>
        /// <param name="viewPoint">Координаты курсора в системе координат представления.</param>
        void DoDrag(Point viewPoint)
        {
            foreach (var selectedElement in model.SelectedElements)
                selectedElement.Location.Offset(viewPoint.X - previousPoint.X, viewPoint.Y - previousPoint.Y);
            view.UpdateView();
        }

        /// <summary>
        /// Определяет, должно ли начаться перетаскивание при перемещении курсора мыши.
        /// </summary>
        /// <param name="viewPoint">Текущие координаты курсора в системе координат представления.</param>
        /// <returns>Возвращает true, если перетаскивание должно начаться, иначе false.</returns>
        bool ShouldStartDrag(Point viewPoint)
        {
            return Math.Abs(clickPoint.X - viewPoint.X) >= DRAG_TOLERANCE_SIZE.Width || (Math.Abs(clickPoint.Y - viewPoint.Y) >= DRAG_TOLERANCE_SIZE.Height);
        }
    }

    /// <summary>
    /// Состояние выделения рамкой.
    /// </summary>
    public class SelectionBoxState : ViewMouseState
    {
        readonly ILayoutView view;
        readonly Point clickPoint;
        
        public SelectionBoxState(NodeLayoutModel model, ILayoutView view, Point viewPoint) : base(model)
        {
            this.view = view;
            clickPoint = viewPoint;
        }

        public override void ProcessMouseMovement(Point p)
        {
            var box = CalculateSelectionBox(model.ClientToView(p));
            model.SelectedElements.Clear();
            foreach (var element in model.GetVisibleElements().Where(element => box.IntersectsWith(element.GetRectangle())))
                model.SelectedElements.Add(element);
            model.SelectionBox = box;
            view.UpdateView();
        }

        Rectangle CalculateSelectionBox(Point viewPoint)
        {
            var left = Math.Min(clickPoint.X, viewPoint.X);
            var top = Math.Min(clickPoint.Y, viewPoint.Y);
            var width = Math.Abs(clickPoint.X - viewPoint.X);
            var height = Math.Abs(clickPoint.Y - viewPoint.Y);
            return new Rectangle(left, top, width, height);
        }

        public override void Leave()
        {
            model.SelectionBox = null;
        }
    }
}
